﻿using System;
using System.Collections.Generic;
using ZoneFiveSoftware.Common.Data.Fitness;
using ZoneFiveSoftware.Common.Visuals.Util;
using ZoneFiveSoftware.Common.Data;
using ZoneFiveSoftware.Common.Data.Measurement;
using ZoneFiveSoftware.Common.Visuals.Fitness;
using STCommon.Controller;
using STCommon.Data;
using DistributionPlugin.Controller;

namespace DistributionPlugin.Data
{
    public class DataProcessing
    {
        public static void FillZoneCategoryForActivitiesAndOptionsHelper(IList<IActivity> activities,
                                                                         DistributionOptions options,
                                                                         IZoneCategory zoneCategory)
        {
            if (options != null && zoneCategory != null &&
                activities != null && activities.Count > 0)
            {
                IApplication app = PluginMain.GetApplication();

                if (options.SelectedDataType != Enums.SportTracksDataTrackTypes.Speed ||
                    options.SpeedReferential != Speed.Units.Pace)
                {
                    float minValue;
                    float maxValue;

                    FindMinMaxForTrackInActivities(activities, options,
                                                   out minValue, out maxValue);

                    Utils.FillZoneCategoryAtInterval(zoneCategory, minValue, maxValue, options.SelectedDataTypeIntervalLength);
                }
                else
                {
                    FillPaceZoneCategoryAtInterval(zoneCategory, activities, options);
                }
            }
        }

        public static void FindMinMaxForTrackInActivities(IList<IActivity> activities, DistributionOptions options,
                                                          out float min, out float max)
        {
            min = 0;
            max = 0;

            if (options != null &&
                Utils.ActivitiesContainDataType(activities, options.SelectedDataType))
            {
                IApplication app = PluginMain.GetApplication();
                Length.Units commonElevationUnit = Utils.GetCommonElevationUnitForActivities(activities, app);
                Length.Units commonDistanceUnit;
                Speed.Units commonSpeedUnit;

                Utils.GetCommonDistanceUnitForActivities(activities, app, out commonDistanceUnit, out commonSpeedUnit);
                min = float.MaxValue;
                max = float.MinValue;

                foreach (IActivity currentActivity in activities)
                {
                    INumericTimeDataSeries dataTrack;

                    if (options.SelectedDataType == Enums.SportTracksDataTrackTypes.HeartRate &&
                        options.HeartRateReferential != HeartRate.PercentTypes.None)
                    {
                        dataTrack = Utils.GetHRTrackInReferential(currentActivity, options.HeartRateReferential,
                                                                  PluginMain.GetApplication().Logbook.Athlete);
                    }
                    else
                    {
                        dataTrack = Utils.GetTrackFromActivity(currentActivity, options.SelectedDataType);
                    }

                    // Make sure to remove stopped times & rest laps to avoid getting too low minimums.  This
                    //  is especially important with pace that could go to infinity if the user stopped moving
                    ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(currentActivity);
                    ValueRangeSeries<DateTime> pausesAndRestLapsRanges = new ValueRangeSeries<DateTime>();

                    // Add pauses to vector
                    foreach (ValueRange<DateTime> currentPause in activityInfo.NonMovingTimes)
                    {
                        pausesAndRestLapsRanges.Add(currentPause);
                    }

                    if (options.ActiveLapsOnly)
                    {
                        // Add rest laps to vector
                        foreach (LapDetailInfo currentLap in activityInfo.RecordedLapDetailInfo)
                        {
                            if (currentLap.Rest)
                            {
                                pausesAndRestLapsRanges.Add(new ValueRange<DateTime>(currentLap.StartTime,
                                                                                     currentLap.EndTime));
                            }
                        }
                    }

                    // Remove all pauses & rest laps from track
                    dataTrack = Utils.RemoveRangesInTrack(dataTrack, pausesAndRestLapsRanges);

                    if (!Utils.IsNullOrEmptyTrack(dataTrack))
                    {
                        float trackMin = dataTrack.Min;
                        float trackMax = dataTrack.Max;
                        float conversionFactor = GetConversionFactor(options.SelectedDataType, options.HeartRateReferential,
                                                                     commonElevationUnit, currentActivity.Category.ElevationUnits,
                                                                     commonDistanceUnit, currentActivity.Category.DistanceUnits,
                                                                     commonSpeedUnit, currentActivity.Category.SpeedUnits);

                        min = Math.Min(min, trackMin * conversionFactor);
                        max = Math.Max(max, trackMax * conversionFactor);
                    }
                }
            }
        }

        public static void FillPaceZoneCategoryAtInterval(IZoneCategory zoneCategory, IList<IActivity> activities, DistributionOptions options)
        {
            if (zoneCategory != null && activities != null && options != null)
            {
                // When using pace with pauses included, we need to handle thing differently because
                //  of the fact that being stopped equals Infinity min/unit.  Consequently, we would
                //  have an infinite number on zones (from minimum to Infinity).
                //
                // Our tactic is therefore to establish the zones based on the highest pace after
                //  removing the stopped times.  We choose the stopped time based on either the category
                //  setting (even if it's ignored) or the ApplicationOptions.MaximumPaceAllowed setting
                if (Utils.ActivitiesContainDataType(activities, options.SelectedDataType))
                {
                    IApplication app = PluginMain.GetApplication();
                    float min = float.MaxValue;
                    float max = float.MinValue;
                    Length.Units commonElevationUnit = Utils.GetCommonElevationUnitForActivities(activities, app);
                    Length.Units commonDistanceUnit;
                    Speed.Units commonSpeedUnit;
                    bool wasStoppedIncluded = app.SystemPreferences.AnalysisSettings.IncludeStopped;

                    app.SystemPreferences.AnalysisSettings.IncludeStopped = false;
                    Utils.GetCommonDistanceUnitForActivities(activities, app, out commonDistanceUnit, out commonSpeedUnit);

                    foreach (IActivity currentActivity in activities)
                    {
                        ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(currentActivity);
                        INumericTimeDataSeries dataTrack = Utils.GetTrackFromActivity(currentActivity,
                                                                                      Enums.SportTracksDataTrackTypes.Speed);

                        if (!Utils.IsNullOrEmptyTrack(dataTrack))
                        {
                            IZoneCategory stoppedTimesCategory = Utils.CreateUnregisteredZoneCategory(app.Logbook);
                            float maxAllowedPace = ApplicationOptions.Instance.MaximumPaceAllowedMetersPerSecond;
                            float conversionFactor = GetConversionFactor(options.SelectedDataType, options.HeartRateReferential,
                                                                         commonElevationUnit, currentActivity.Category.ElevationUnits,
                                                                         commonDistanceUnit, currentActivity.Category.DistanceUnits,
                                                                         commonSpeedUnit, currentActivity.Category.SpeedUnits);

                            // Determine stopped pace.  Use the configured value if above 0
                            bool wasUsingParentSettings = currentActivity.Category.UseParentSettings;
                            float oldStoppedSpeed = currentActivity.Category.StoppedMetersPerSecond;
                            bool resetValues = false;
                            if (currentActivity.Category.StoppedMetersPerSecond == 0)
                            {
                                currentActivity.Category.UseParentSettings = false;
                                currentActivity.Category.StoppedMetersPerSecond = maxAllowedPace;
                                resetValues = true;
                            }

                            // Remove non moving times from the dataTrack
                            ActivityInfo workingActivityInfo = new ActivityInfo(currentActivity, new PaceDistributionActivityInfoOptions());
                            ValueRangeSeries<DateTime> pausesAndRestLapsRanges = new ValueRangeSeries<DateTime>();

                            // Add pauses to vector
                            foreach (ValueRange<DateTime> currentPause in activityInfo.NonMovingTimes)
                            {
                                pausesAndRestLapsRanges.Add(currentPause);
                            }

                            if (options.ActiveLapsOnly)
                            {
                                // Add rest laps to vector
                                foreach (LapDetailInfo currentLap in activityInfo.RecordedLapDetailInfo)
                                {
                                    if (currentLap.Rest)
                                    {
                                        pausesAndRestLapsRanges.Add(new ValueRange<DateTime>(currentLap.StartTime,
                                                                                             currentLap.EndTime));
                                    }
                                }
                            }

                            // Remove all pauses & rest laps from track
                            dataTrack = Utils.RemoveRangesInTrack(dataTrack, pausesAndRestLapsRanges);

                            // Use the min & max from the remainder of the track
                            min = Math.Min(min, dataTrack.Min * conversionFactor);
                            max = Math.Max(max, dataTrack.Max * conversionFactor);

                            // Reset stopped speed
                            if (resetValues)
                            {
                                currentActivity.Category.StoppedMetersPerSecond = oldStoppedSpeed;
                                currentActivity.Category.UseParentSettings = wasUsingParentSettings;
                            }
                        }
                    }

                    app.SystemPreferences.AnalysisSettings.IncludeStopped = wasStoppedIncluded;

                    // Now that we have the min & max, fill the zone category
                    Utils.FillZoneCategoryAtInterval(zoneCategory, min, max, options.SelectedDataTypeIntervalLength);

                    // Add the stopped times zone and we are done
                    if (zoneCategory.Zones.Count > 0)
                    {
                        zoneCategory.Zones.Add(new NamedLowHighZone("Stopped Times",
                                                                    zoneCategory.Zones[zoneCategory.Zones.Count - 1].High,
                                                                    float.PositiveInfinity));
                    }
                    else
                    {
                        // We we're unable to create the zones above.  This means min or max was infinity, use
                        //  them values directly
                        zoneCategory.Zones.Add(new NamedLowHighZone("Stopped Times", min, max));
                    }
                }
            }
        }

        public static IDictionary<INamedLowHighZone, DistributionZoneInfo> GetCumulativeInfoForActivities(IList<IActivity> activities,
                                                                                                          DistributionOptions options,
                                                                                                          IZoneCategory categories)
        {
            Dictionary<INamedLowHighZone, DistributionZoneInfo> result = new Dictionary<INamedLowHighZone, DistributionZoneInfo>();

            if (categories != null)
            {
                foreach (INamedLowHighZone currentNamedZone in categories.Zones)
                {
                    result[currentNamedZone] = new DistributionZoneInfo(currentNamedZone);
                }

                if (options != null && activities != null)
                {
                    IApplication app = PluginMain.GetApplication();
                    IList<IActivity> uniqueActivitiesList = Utils.RemoveDuplicatesFromList(activities);
                    Length.Units commonElevationUnit = Utils.GetCommonElevationUnitForActivities(uniqueActivitiesList, app);
                    Length.Units commonDistanceUnit;
                    Speed.Units commonSpeedUnit;

                    Utils.GetCommonDistanceUnitForActivities(uniqueActivitiesList, app, out commonDistanceUnit, out commonSpeedUnit);

                    foreach (IActivity currentActivity in uniqueActivitiesList)
                    {
                        if (Utils.ActivityContainsDataType(currentActivity, options.SelectedDataType))
                        {
                            float conversionFactor = GetConversionFactor(options.SelectedDataType,
                                                                         options.HeartRateReferential,
                                                                         commonElevationUnit, currentActivity.Category.ElevationUnits,
                                                                         commonDistanceUnit, currentActivity.Category.DistanceUnits,
                                                                         commonSpeedUnit, currentActivity.Category.SpeedUnits);

                            if (options.ActiveLapsOnly)
                            {
                                ActiveLapsOnlyActivityDistributionInfoHelper(currentActivity, options,
                                                                             categories, conversionFactor,
                                                                             ref result);
                            }
                            else
                            {
                                StandardActivityDistributionInfoHelper(currentActivity, options,
                                                                       categories, conversionFactor,
                                                                       ref result);
                            }
                        }
                    }
                }
            }

            return result;
        }


        private static void StandardActivityDistributionInfoHelper(IActivity activity,
                                                                   DistributionOptions options,
                                                                   IZoneCategory categories,
                                                                   float conversionFactor,
                                                                   ref Dictionary<INamedLowHighZone, DistributionZoneInfo> result)
        {
            ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(activity);;
            ZoneCategoryInfo zonesInfo;
            INumericTimeDataSeries dataTrack;

            if (options.SelectedDataType == Enums.SportTracksDataTrackTypes.HeartRate &&
                options.HeartRateReferential != HeartRate.PercentTypes.None)
            {
                dataTrack = Utils.GetHRTrackInReferential(activity, options.HeartRateReferential, PluginMain.GetApplication().Logbook.Athlete);
                conversionFactor = 1.0f;
            }
            else
            {
                dataTrack = Utils.GetTrackFromActivity(activity, options.SelectedDataType);
            }

            zonesInfo = ZoneCategoryInfo.Calculate(activityInfo, categories, dataTrack, conversionFactor);

            foreach (ZoneInfo currentZoneInfo in zonesInfo.Zones)
            {
                // Make sure not to take the totals info
                if (result.ContainsKey(currentZoneInfo.Zone))
                {
                    result[currentZoneInfo.Zone] += currentZoneInfo;
                } 
            }
        }

        private static void ActiveLapsOnlyActivityDistributionInfoHelper(IActivity activity,
                                                                         DistributionOptions options,
                                                                         IZoneCategory categories,
                                                                         float conversionFactor,
                                                                         ref Dictionary<INamedLowHighZone, DistributionZoneInfo> result)
        {
            ActivityInfo activityInfo = ActivityInfoCache.Instance.GetInfo(activity);;
            ZoneCategoryInfo zonesInfo;
            INumericTimeDataSeries dataTrack;
            ValueRangeSeries<DateTime> activeLapsOnlyTimes = new ValueRangeSeries<DateTime>();

            if (options.SelectedDataType == Enums.SportTracksDataTrackTypes.HeartRate &&
                options.HeartRateReferential != HeartRate.PercentTypes.None)
            {
                dataTrack = Utils.GetHRTrackInReferential(activity, options.HeartRateReferential, PluginMain.GetApplication().Logbook.Athlete);
                conversionFactor = 1.0f;
            }
            else
            {
                dataTrack = Utils.GetTrackFromActivity(activity, options.SelectedDataType);
            }

            // Isolate each active lap from the track to analyse
            foreach (LapDetailInfo currentLap in activityInfo.RecordedLapDetailInfo)
            {
                if (!currentLap.Rest)
                {
                    INumericTimeDataSeries isolatedDataTrack = Utils.IsolateRangeInTrack(dataTrack, new ValueRange<DateTime>(currentLap.StartTime,
                                                                                              currentLap.EndTime));
                    zonesInfo = ZoneCategoryInfo.Calculate(activityInfo, categories, isolatedDataTrack, conversionFactor);

                    foreach (ZoneInfo currentZoneInfo in zonesInfo.Zones)
                    {
                        // Make sure not to take the totals info
                        if (result.ContainsKey(currentZoneInfo.Zone))
                        {
                            result[currentZoneInfo.Zone] += currentZoneInfo;
                        }
                    }
                }
            }
        }

        private static float GetConversionFactor(Enums.SportTracksDataTrackTypes dataType,
                                                HeartRate.PercentTypes HRReferential,
                                                Length.Units commonElevation, Length.Units activityElevation,
                                                Length.Units commonDistance, Length.Units activityDistance,
                                                Speed.Units commonSpeed, Speed.Units activitySpeed)
        {
            float conversionFactor = 1.0f;

            if (dataType == Enums.SportTracksDataTrackTypes.Grade)
            {
                conversionFactor = 100.0f;
            }
            else if (dataType == Enums.SportTracksDataTrackTypes.Elevation &&
                     activityElevation != commonElevation)
            {
                conversionFactor = (float)Length.Convert(1.0, activityElevation, commonElevation);
            }
            else if (dataType == Enums.SportTracksDataTrackTypes.Speed &&
                     activityDistance != commonDistance)
            {
                conversionFactor = (float)Length.Convert(1.0, activityDistance, commonDistance);

                // Invert if mismatch between pace & speed
                if (commonSpeed != activitySpeed)
                {
                    conversionFactor = 1 / conversionFactor;
                }
            }

            return conversionFactor;
        }
    }
}
